﻿using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Expect.Core;
using System;
using Expect.Core.BaseClasses;
using Expect.Core.Interfaces;
using Expect.Core.IO;

namespace Expect.Core.CodeGen
{
    /// <summary>
    /// Code generator
    /// </summary>
    public class ExpectTestGenerator
    {
        private const string beforeinitevent = "BeforeInit";

        private const string afterinitevent = "AfterInit";

        private const string beforeteardownevent = "BeforeTeardown";

        private const string afterteardownevent = "AfterTeardown";

        private const string preparecontextevent = "PrepareContext";

        private INameProvider _nameprovider;

        private GenerationInfo _info;

        private ParsingResult _parseInfo;

        private CodeDomHelper codeDomHelper;

        
        /// <summary>
        /// Attribute for class header
        /// </summary>
        public string ClassHeaderAttribute
        {
            get
            {
                if (_info.Config.Framework == UnitTestProviders.NUnit)
                {
                    return "TestFixture";
                }

                return "TestClass";
            }
        }

        /// <summary>
        /// Attribute for test init
        /// </summary>
        public string TestInitAttribute
        {
            get
            {
                if (_info.Config.Framework == UnitTestProviders.NUnit)
                {
                    return "SetUp";
                }

                return "TestInitialize";
            }
        }

        /// <summary>
        /// Attribute for test cleanup
        /// </summary>
        public string TestCleanupAttribute
        {
            get
            {
                if (_info.Config.Framework == UnitTestProviders.NUnit)
                {
                    return "TearDown";
                }

                return "TestCleanup";
            }
        }

        /// <summary>
        /// Attribute for test methods
        /// </summary>
        public string TestMethodAttribute
        {
            get
            {
                if (_info.Config.Framework == UnitTestProviders.NUnit)
                {
                    return "Test";
                }

                return "TestMethod";
            }
        }

        /// <summary>
        /// Code generator constructor
        /// </summary>
        /// <param name="info"></param>
        /// <param name="parseInfo"></param>
        public ExpectTestGenerator(GenerationInfo info, ParsingResult parseInfo)
        {
            _parseInfo = parseInfo;
            _info = info;
            _nameprovider = new DefaultNameProvider();
        }

        private CodeGeneratorOptions GenerationOptions()
        {
            CodeGeneratorOptions options = new CodeGeneratorOptions();
            options.BracingStyle = "C";
            return options;
        }

        /// <summary>
        /// Generate code
        /// </summary>
        /// <returns></returns>
        public string Execute()
        {
            return this.DoExecute(false);
        }

        /// <summary>
        /// Generate code
        /// </summary>
        /// <returns></returns>
        public string ExecuteContainer()
        {
            return this.DoExecute(true);
        }

        /// <summary>
        /// Generate code
        /// </summary>
        /// <returns></returns>
        private string DoExecute(bool isContainer)
        {
            CodeNamespace codeNamespace = new CodeNamespace(_info.CodeFileNameSpace);
            codeDomHelper = new CodeDomHelper(_info.Provider);
            var nameprovider = new DefaultNameProvider();

            StringBuilder buffer = new StringBuilder();
            using (var writer = new StringWriter(buffer))
            {
                GenerateUsings(codeNamespace, isContainer);

                var classname = nameprovider.ElementName(_info.CodeFileName);
                var generatedclass = codeDomHelper.CreateGeneratedTypeDeclaration(classname + AbstractSpecificationContainer.ContainerClassSuffix, isContainer);                
                
                generatedclass.IsPartial = true;
                generatedclass.TypeAttributes |= TypeAttributes.Public;
                codeNamespace.Types.Add(generatedclass);

                if (isContainer)
                {
                    generatedclass.Comments.Add(
                        new CodeCommentStatement(
                            "This file is intended to contain your fixtures, you could modify it's content freely because it won't be generated again"));
                    generatedclass.Comments.Add(
                        new CodeCommentStatement(
                            "Please note that if you rename the parent .expect file, this file will not be renamed automatically"));
                }
                else
                {
                    var basename = GetBaseClassName();

                    generatedclass.BaseTypes.Add(new CodeTypeReference(basename));
                
                    string codefilepath = _info.CodeFilePath.Substring(_info.ProjectFolder.Length);
                    generatedclass.CustomAttributes.Add(new CodeAttributeDeclaration(ClassHeaderAttribute));
                    generatedclass.CustomAttributes.Add(
                        new CodeAttributeDeclaration(typeof(SpecificationContainerAttribute).Name, 
                                new CodeAttributeArgument("Name", new CodePrimitiveExpression(_info.CodeFileName)),
                                new CodeAttributeArgument("Path", new CodePrimitiveExpression(codefilepath))
                            )
                        );
                    codeDomHelper.BindTypeToSourceFile(generatedclass, Path.GetFileName(_info.CodeFilePath));

                    generatedclass.Comments.Add(
                        new CodeCommentStatement(
                            "This file is generated, if you modify content you will lose your modifications when the generator will regenerate the file"));

                    //GenerateContextProperty(generatedclass);

                    GeneratePartialMethods(generatedclass);

                    GenerateClassConstructor(generatedclass);

                    GenerateContainerMethod(generatedclass);

                    GenerateTestInit(generatedclass);

                    GenerateTestTeardown(generatedclass);

                    this.GenerateSpecification(_parseInfo.Specification, generatedclass, TestMethodAttribute);
                }

                _info.Provider.GenerateCodeFromNamespace(codeNamespace, writer, GenerationOptions());
            }

            return buffer.ToString();
        }

        private string GetBaseClassName()
        {
            if (!string.IsNullOrEmpty(_info.Config.ContainerBaseClass))
            {
                return _info.Config.ContainerBaseClass;
            }

            if (!string.IsNullOrEmpty(this._parseInfo.ContainerBaseClass))
            {
                return _parseInfo.ContainerBaseClass;
            }

            return typeof(AbstractSpecificationContainer).FullName;
        }

        private void GenerateUsings(CodeNamespace codeNamespace, bool isContainer)
        {
            codeNamespace.Imports.Add(new CodeNamespaceImport("System"));
            codeNamespace.Imports.Add(new CodeNamespaceImport("Expect.Core"));
            codeNamespace.Imports.Add(new CodeNamespaceImport("Expect.Core.Fixtures"));

            if (_info.Config.Framework == UnitTestProviders.MsTest)
            {
                codeNamespace.Imports.Add(new CodeNamespaceImport("Microsoft.VisualStudio.TestTools.UnitTesting"));
            }
            if (_info.Config.Framework == UnitTestProviders.NUnit)
            {
                codeNamespace.Imports.Add(new CodeNamespaceImport("NUnit.Framework"));
            }
        }

        private void GenerateContainerMethod(CodeTypeDeclaration generatedclass)
        {
            CodeMemberMethod memberMethod = new CodeMemberMethod();
            memberMethod.Attributes = MemberAttributes.Public | MemberAttributes.Override;
            memberMethod.Name = "InitializeContext";
            memberMethod.ReturnType = new CodeTypeReference(typeof(void));

            memberMethod.Statements.Add(
                new CodeMethodInvokeExpression(                        
                        new CodeBaseReferenceExpression(),
                        "InitializeContext"
                    ));

            memberMethod.Statements.Add(new CodeMethodInvokeExpression(null, preparecontextevent + "Extension"));

            generatedclass.Members.Add(memberMethod);
        }

        private void GenerateTestTeardown(CodeTypeDeclaration generatedclass)
        {
            var methodname = _nameprovider.ElementName(_info.CodeFileName) + "Class_TestTeardown";
            this.GenerateTestPreparation(
                generatedclass,
                _parseInfo.Specification.Teardown,
                TestCleanupAttribute,
                methodname,
                beforeteardownevent,
                afterteardownevent,
                typeof(TeardownProviderAttribute),
                null,
                null,
                method =>
                    {
                        if (!string.IsNullOrEmpty(_parseInfo.GetTeardownFrom))
                        {
                            //method.Statements.Add(new CodeVariableDeclarationStatement(typeof(SpecificationDefinition), "spec"));
                            method.Statements.Add(
                                new CodeMethodInvokeExpression(
                                    new CodePropertyReferenceExpression(null, "Context"),
                                    "GetTeardownFrom",
                                    new CodeVariableReferenceExpression("spec"),
                                    new CodePrimitiveExpression(_parseInfo.GetTeardownFrom)));
                            //this.GenerateTestConclusion(method);
                        }
                    });
        }

        private void GenerateTestInit(CodeTypeDeclaration generatedclass)
        {
            var methodName = _nameprovider.ElementName(_info.CodeFileName) + "Class_TestInit";
            this.GenerateTestPreparation(generatedclass, _parseInfo.Specification.Setup, TestInitAttribute, methodName, beforeinitevent, afterinitevent, typeof(SetupProviderAttribute),
                method =>
                    {
                        method.Statements.Add(new CodeMethodInvokeExpression(null, "InitializeContext"));
                    },
                method =>
                    {
                        if (!string.IsNullOrEmpty(_parseInfo.GetSetupFrom))
                        {
                            //method.Statements.Add(new CodeVariableDeclarationStatement(typeof(SpecificationDefinition), "spec"));
                            method.Statements.Add(
                                new CodeMethodInvokeExpression(
                                    new CodePropertyReferenceExpression(null, "Context"),
                                    "GetSetupFrom",
                                    new CodeVariableReferenceExpression("spec"),
                                    new CodePrimitiveExpression(_parseInfo.GetSetupFrom)));
                            //this.GenerateTestConclusion(method);
                        }
                    },
                null);
        }

        private void GenerateTestPreparation(
            CodeTypeDeclaration generatedclass, 
            SpecificationDefinition spec, 
            string attrname, 
            string methodname, 
            string beforeevent, 
            string afterevent, 
            Type mmethAttr, 
            Action<CodeMemberMethod> action,
            Action<CodeMemberMethod> customactionBefore,
            Action<CodeMemberMethod> customactionAfter)
        {
            CodeMemberMethod memberMethod = new CodeMemberMethod();
            memberMethod.Attributes = MemberAttributes.Public /*| MemberAttributes.Override*/;
            memberMethod.CustomAttributes.Add(new CodeAttributeDeclaration(attrname));
            memberMethod.Name = methodname;
            memberMethod.ReturnType = new CodeTypeReference(typeof(void));
            generatedclass.Members.Add(memberMethod);

            if (action != null)
            {
                action(memberMethod);
            }

            memberMethod.Statements.Add(new CodeMethodInvokeExpression(null, beforeevent));
            memberMethod.Statements.Add(new CodeMethodInvokeExpression(null, beforeevent + "Extension"));

            memberMethod.Statements.Add(new CodeSnippetStatement(string.Empty));

            memberMethod.Statements.Add(
                new CodeVariableDeclarationStatement(
                    "SpecificationDefinition", "spec", new CodeObjectCreateExpression("SpecificationDefinition")));

            if (customactionBefore != null)
                customactionBefore(memberMethod);

            if (spec != null && spec.Fixtures.Count > 0)
            {
                this.GenerateSpecResolution(memberMethod, spec);
                this.GenerateSpecificationMethodContent(generatedclass, spec, mmethAttr);
            }

            if (customactionAfter != null)
                customactionAfter(memberMethod);

            this.GenerateTestConclusion(memberMethod);

            memberMethod.Statements.Add(new CodeSnippetStatement(string.Empty));

            memberMethod.Statements.Add(new CodeMethodInvokeExpression(null, afterevent));
            memberMethod.Statements.Add(new CodeMethodInvokeExpression(null, afterevent + "Extension"));
        }

        private void GeneratePartialMethods(CodeTypeDeclaration generatedclass)
        {
            GeneratePartialMethod(generatedclass, preparecontextevent);
            GeneratePartialMethod(generatedclass, beforeinitevent);
            GeneratePartialMethod(generatedclass, afterinitevent);
            GeneratePartialMethod(generatedclass, beforeteardownevent);
            GeneratePartialMethod(generatedclass, afterteardownevent);
        }

        private void GeneratePartialMethod(CodeTypeDeclaration generatedclass, string methodname)
        {
            var method = new CodeMemberField();
            method.Attributes = MemberAttributes.ScopeMask;
            method.Name = methodname + "Extension()";
            method.Type = new CodeTypeReference("partial void");

            generatedclass.Members.Add(method);
        }

        private void GenerateContextProperty(CodeTypeDeclaration generatedclass)
        {
            CodeMemberField fieldContext = new CodeMemberField(typeof(IExecutionContext), "_context");
            generatedclass.Members.Add(fieldContext);

            CodeMemberProperty declarContext = new CodeMemberProperty();
            declarContext.Attributes = MemberAttributes.Public;
            declarContext.Type = new CodeTypeReference(typeof(IExecutionContext));
            declarContext.Name = "Context";
            declarContext.HasGet = true;
            declarContext.GetStatements.Add(new CodeSnippetExpression("return _context"));

            generatedclass.Members.Add(declarContext);
        }

        private void GenerateClassConstructor(CodeTypeDeclaration generatedclass)
        {

        }

        private void GenerateSpecification(SpecificationDefinition spec, CodeTypeDeclaration generatedclass, string testAttribute)
        {
            if (spec != null && spec.Fixtures.Count > 0)
            {
                var method = this.GenerateSpecificationTestMethod(spec, generatedclass, testAttribute);
                this.GenerateSpecificationMethodContent(generatedclass, spec, typeof(SpecificationProviderAttribute));
            }

            foreach (var childSpec in spec.ChildSpecs)
            {
                this.GenerateSpecification(childSpec, generatedclass, testAttribute);
            }
        }

        private void GenerateTestConclusion(CodeMemberMethod method)
        {
            method.Statements.Add(
                    new CodeMethodInvokeExpression(
                            new CodeVariableReferenceExpression("spec"),
                            "CalculateResult")
                        );
            method.Statements.Add(new CodeSnippetStatement(string.Empty));
            this.InconclusiveTest(method);
            this.FailedTest(method);
        }


        private void FailedTest(CodeMemberMethod method)
        {
            var elt =
                new CodePropertyReferenceExpression(
                    new CodePropertyReferenceExpression(
                        new CodePropertyReferenceExpression(
                            new CodeArgumentReferenceExpression("spec"),
                            "Status"),
                        "Success")
                , "Value");


            CodeBinaryOperatorExpression condition = new CodeBinaryOperatorExpression(
                    elt,
                    CodeBinaryOperatorType.ValueEquality,
                    new CodePrimitiveExpression(false)
                );
            var failed =
                new CodeMethodInvokeExpression(
                    new CodeTypeReferenceExpression("Assert"),
                    "Fail",
                    new CodeMethodInvokeExpression(
                            new CodeVariableReferenceExpression("spec"),
                            "GetMessageForFailedResult")
                        );
            CodeConditionStatement ifStatement = new CodeConditionStatement();
            ifStatement.Condition = condition;
            ifStatement.TrueStatements.Add(failed);

            method.Statements.Add(ifStatement);
        }

        private void InconclusiveTest(CodeMemberMethod method)
        {
            var elt =
                new CodePropertyReferenceExpression(
                    new CodePropertyReferenceExpression(
                        new CodePropertyReferenceExpression(
                            new CodeArgumentReferenceExpression("spec"),
                            "Status"),
                        "Success")
                , "HasValue");
            CodeBinaryOperatorExpression condition = new CodeBinaryOperatorExpression(
                    elt,
                    CodeBinaryOperatorType.ValueEquality,
                    new CodePrimitiveExpression(false)
                );
            var inconclusiveMethod = "Inconclusive";
            if (_info.Config.Framework == UnitTestProviders.NUnit) inconclusiveMethod = "Ignore";

            var inconclusive =
                new CodeMethodInvokeExpression(
                    new CodeTypeReferenceExpression("Assert"),
                    inconclusiveMethod,
                    new CodeMethodInvokeExpression(
                            new CodeVariableReferenceExpression("spec"),
                            "GetMessageForInconclusiveResult")
                        );
            CodeConditionStatement ifStatement = new CodeConditionStatement();
            ifStatement.Condition = condition;
            ifStatement.TrueStatements.Add(inconclusive);

            method.Statements.Add(ifStatement);
        }

        private CodeMemberMethod GenerateSpecificationTestMethod(SpecificationDefinition spec, CodeTypeDeclaration generatedclass, string testAttribute)
        {
            CodeMemberMethod memberMethod = new CodeMemberMethod();
            memberMethod.Attributes = MemberAttributes.Public /*| MemberAttributes.Override*/;
            memberMethod.CustomAttributes.Add(new CodeAttributeDeclaration(testAttribute));
            //Make this method an override of base class's method
            memberMethod.Comments.Add(new CodeCommentStatement(new CodeComment("TestComment")));
            var codeName = _nameprovider.ElementName(_info.CodeFileName);
            var specName = _nameprovider.ElementName(spec.Name);
            var methodName = specName;
            if (specName != codeName) 
                methodName = codeName + "_" + specName;
            memberMethod.Name = methodName;
            memberMethod.ReturnType = new CodeTypeReference(typeof(void));
            generatedclass.Members.Add(memberMethod);

            memberMethod.Statements.Add(
                new CodeVariableDeclarationStatement(
                    "SpecificationDefinition", "spec", new CodeObjectCreateExpression("SpecificationDefinition")));

            GenerateSpecResolution(memberMethod, spec);
            this.GenerateTestConclusion(memberMethod); 
            
            return memberMethod;
        }

        private void GenerateSpecResolution(CodeMemberMethod memberMethod, SpecificationDefinition spec)
        {            
            memberMethod.Statements.Add(
                new CodeMethodInvokeExpression(
                    null,
                    "Get_" + this._nameprovider.ElementName(spec.Name),
                    new CodeVariableReferenceExpression("spec"))
                );
        }

        private void GenerateSpecificationMethodContent(CodeTypeDeclaration generatedclass, SpecificationDefinition spec, Type attrType)
        {
            CodeMemberMethod method = new CodeMemberMethod();
            method.Attributes = MemberAttributes.Public /*| MemberAttributes.Override*/;
            method.CustomAttributes.Add(new CodeAttributeDeclaration(
                attrType.Name,
                new CodeAttributeArgument("Name", new CodePrimitiveExpression(spec.Name))));
            method.Name = "Get_" + _nameprovider.ElementName(spec.Name);
            
            generatedclass.Members.Add(method);
            method.Parameters.Add(new CodeParameterDeclarationExpression(typeof(SpecificationDefinition), "spec"));
            
            method.Statements.Add(
                new CodeAssignStatement(
                    new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("spec"), "Context"),
                    new CodeFieldReferenceExpression() { FieldName = "Context" }));

            method.Statements.Add(
                new CodeAssignStatement(
                    new CodePropertyReferenceExpression(new CodeVariableReferenceExpression("spec"), "Name"),
                    new CodePrimitiveExpression(spec.Name)));

            method.Statements.Add(
                new CodeVariableDeclarationStatement("FixtureDefinition", "fixture", new CodePrimitiveExpression(null)));

            method.Statements.Add(new CodeSnippetStatement(string.Empty));

            foreach (var fixture in spec)
            {
                WriteFixture(method, fixture);
                method.Statements.Add(new CodeSnippetStatement(string.Empty));
            }

            /*
            method.Statements.Add(
                  new CodeMethodInvokeExpression(
                          new CodeVariableReferenceExpression("spec"),
                          "EndRun")
                      );  */         
        }

        private void WriteFixture(CodeMemberMethod method, FixtureDefinition fixture)
        {
            CodeMethodInvokeExpression fixturemeth =
                new CodeMethodInvokeExpression(
                            new CodeVariableReferenceExpression("spec"),
                            "AddFixture");

            if (fixture.HasClass)
            {
                fixturemeth.Method.TypeArguments.Add(fixture.ClassType.Name);
            }

            System.Text.StringBuilder sb = new StringBuilder();
            method.Statements.Add(
            new CodeAssignStatement(
                    new CodeVariableReferenceExpression("fixture"),
                    fixturemeth));

            foreach (var row in fixture)
            {
                codeDomHelper.AddSourceLinePragmaStatement(method.Statements, row.MetaData.LineNumber);
                method.Statements.Add(
                   new CodeMethodInvokeExpression(
                           new CodeVariableReferenceExpression("fixture"),
                           "WithRow", RowString(row))
                       );
            }

            codeDomHelper.AddDisableSourceLinePragmaStatement(method.Statements);

            method.Statements.Add(
                   new CodeMethodInvokeExpression(
                           new CodeVariableReferenceExpression("fixture"),
                           "EndRun")
                       );
        }

        private void AddDescription(StringBuilder sb, string descname, string desc)
        {
            if (!string.IsNullOrEmpty(desc))
            {
                sb.AppendLine("." + descname + "(\"" + desc + "\")");
            }
        }

        private CodePrimitiveExpression[] RowString(FixtureRow row)
        {
            return row.Cells.Select(c => new CodePrimitiveExpression(c.Text)).ToArray();
        }
    }
}
